baubs git
Commit 469c08d45bbf92488a10ae97080d8533460c860e
Parents : 99d32f9
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-25T21:29:24+01:00
feature: Create unified PatternLayer component to eliminate duplication
Unified three separate layer implementations into single PatternLayer component:
**PatternLayer.tsx (156 lines)**
- Handles both interactive (original) and locked (uploaded) pattern rendering
- Props-driven configuration for dragging, rotation, transformer
- Includes CurrentPosition indicator for uploaded patterns
- Memoized stitches and center calculations for performance
- Single source of truth for pattern rendering logic
**Benefits:**
- Eliminated ~140 lines of duplicated layer code
- Reduced PatternCanvas from 388 to 271 lines (-117 lines, -30%)
- Consistent rendering behavior across pattern states
- Easier to maintain and test pattern rendering
- Cleaner component composition in PatternCanvas
**Removed from PatternCanvas:**
- Original pattern layer implementation (74 lines)
- Uploaded pattern layer implementation (33 lines)
- Current position layer implementation (26 lines)
- Duplicate Group, Transformer, Stitches setup
- Redundant center calculations and stitch conversions
**Complete refactoring impact (All Phases):**
- Original: 786 lines in single monolithic file
- After Phase 4: 271 lines main + hooks + components
- **Total reduction: -515 lines (-65%)**
File structure now:
- PatternCanvas.tsx: 271 lines (orchestration)
- PatternLayer.tsx: 156 lines (rendering)
- useCanvasViewport.ts: 166 lines (viewport)
- usePatternTransform.ts: 179 lines (transform)
- 3 display components + 3 utilities
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes
2 files changed, 183 insertions(+), 144 deletions(-)
Diff
diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx
index 665fb69..88c9c5f 100644
--- a/src/components/PatternCanvas/PatternCanvas.tsx
+++ b/src/components/PatternCanvas/PatternCanvas.tsx
@@ -5,17 +5,10 @@ import {
usePatternUploaded,
} from "../../stores/useMachineStore";
import { usePatternStore } from "../../stores/usePatternStore";
-import { Stage, Layer, Group, Transformer } from "react-konva";
+import { Stage, Layer } from "react-konva";
import Konva from "konva";
import { PhotoIcon } from "@heroicons/react/24/solid";
-import {
- Grid,
- Origin,
- Hoop,
- Stitches,
- PatternBounds,
- CurrentPosition,
-} from "../KonvaComponents";
+import { Grid, Origin, Hoop } from "../KonvaComponents";
import {
Card,
CardHeader,
@@ -23,13 +16,10 @@ import {
CardDescription,
CardContent,
} from "@/components/ui/card";
-import {
- calculatePatternCenter,
- convertPenStitchesToPesFormat,
-} from "./patternCanvasHelpers";
import { ThreadLegend } from "./ThreadLegend";
import { PatternPositionIndicator } from "./PatternPositionIndicator";
import { ZoomControls } from "./ZoomControls";
+import { PatternLayer } from "./PatternLayer";
import { useCanvasViewport } from "../../hooks/useCanvasViewport";
import { usePatternTransform } from "../../hooks/usePatternTransform";
@@ -201,141 +191,34 @@ export function PatternCanvas() {
{/* Original pattern layer: draggable with transformer (shown before upload starts) */}
<Layer visible={!isUploading && !patternUploaded}>
- {pesData &&
- (() => {
- const originalCenter = calculatePatternCenter(
- pesData.bounds,
- );
- console.log("[Canvas] Rendering original pattern:", {
- position: localPatternOffset,
- rotation: localPatternRotation,
- center: originalCenter,
- bounds: pesData.bounds,
- });
- return (
- <>
- <Group
- name="pattern-group"
- ref={(node) => {
- patternGroupRef.current = node;
- // Set initial rotation from state
- if (node) {
- node.rotation(localPatternRotation);
- // Try to attach transformer when group is mounted
- attachTransformer();
- }
- }}
- draggable={!isUploading}
- x={localPatternOffset.x}
- y={localPatternOffset.y}
- offsetX={originalCenter.x}
- offsetY={originalCenter.y}
- onDragEnd={handlePatternDragEnd}
- onTransformEnd={handleTransformEnd}
- onMouseEnter={(e) => {
- const stage = e.target.getStage();
- if (stage && !isUploading)
- stage.container().style.cursor = "move";
- }}
- onMouseLeave={(e) => {
- const stage = e.target.getStage();
- if (stage && !isUploading)
- stage.container().style.cursor = "grab";
- }}
- >
- <Stitches
- stitches={convertPenStitchesToPesFormat(
- pesData.penStitches,
- )}
- pesData={pesData}
- currentStitchIndex={0}
- showProgress={false}
- />
- <PatternBounds bounds={pesData.bounds} />
- </Group>
- <Transformer
- ref={(node) => {
- transformerRef.current = node;
- // Try to attach transformer when transformer is mounted
- if (node) {
- attachTransformer();
- }
- }}
- enabledAnchors={[]}
- rotateEnabled={true}
- borderEnabled={true}
- borderStroke="#FF6B6B"
- borderStrokeWidth={2}
- rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]}
- ignoreStroke={true}
- rotateAnchorOffset={20}
- />
- </>
- );
- })()}
+ {pesData && (
+ <PatternLayer
+ pesData={pesData}
+ offset={localPatternOffset}
+ rotation={localPatternRotation}
+ isInteractive={true}
+ showProgress={false}
+ currentStitchIndex={0}
+ patternGroupRef={patternGroupRef}
+ transformerRef={transformerRef}
+ onDragEnd={handlePatternDragEnd}
+ onTransformEnd={handleTransformEnd}
+ attachTransformer={attachTransformer}
+ />
+ )}
</Layer>
{/* Uploaded pattern layer: locked, rotation baked in (shown during and after upload) */}
<Layer visible={isUploading || patternUploaded}>
- {uploadedPesData &&
- (() => {
- const uploadedCenter = calculatePatternCenter(
- uploadedPesData.bounds,
- );
- console.log("[Canvas] Rendering uploaded pattern:", {
- position: initialUploadedPatternOffset,
- center: uploadedCenter,
- bounds: uploadedPesData.bounds,
- });
- return (
- <Group
- name="uploaded-pattern-group"
- x={initialUploadedPatternOffset.x}
- y={initialUploadedPatternOffset.y}
- offsetX={uploadedCenter.x}
- offsetY={uploadedCenter.y}
- >
- <Stitches
- stitches={convertPenStitchesToPesFormat(
- uploadedPesData.penStitches,
- )}
- pesData={uploadedPesData}
- currentStitchIndex={
- sewingProgress?.currentStitch || 0
- }
- showProgress={true}
- />
- <PatternBounds bounds={uploadedPesData.bounds} />
- </Group>
- );
- })()}
- </Layer>
-
- {/* Current position layer (for uploaded pattern during sewing) */}
- <Layer visible={isUploading || patternUploaded}>
- {uploadedPesData &&
- sewingProgress &&
- sewingProgress.currentStitch > 0 &&
- (() => {
- const center = calculatePatternCenter(
- uploadedPesData.bounds,
- );
- return (
- <Group
- x={initialUploadedPatternOffset.x}
- y={initialUploadedPatternOffset.y}
- offsetX={center.x}
- offsetY={center.y}
- >
- <CurrentPosition
- currentStitchIndex={sewingProgress.currentStitch}
- stitches={convertPenStitchesToPesFormat(
- uploadedPesData.penStitches,
- )}
- />
- </Group>
- );
- })()}
+ {uploadedPesData && (
+ <PatternLayer
+ pesData={uploadedPesData}
+ offset={initialUploadedPatternOffset}
+ isInteractive={false}
+ showProgress={true}
+ currentStitchIndex={sewingProgress?.currentStitch || 0}
+ />
+ )}
</Layer>
</Stage>
)}
diff --git a/src/components/PatternCanvas/PatternLayer.tsx b/src/components/PatternCanvas/PatternLayer.tsx
new file mode 100644
index 0000000..7d1f4ff
--- /dev/null
+++ b/src/components/PatternCanvas/PatternLayer.tsx
@@ -0,0 +1,156 @@
+/**
+ * PatternLayer Component
+ *
+ * Unified component for rendering pattern layers (both original and uploaded)
+ * Handles both interactive (draggable/rotatable) and locked states
+ */
+
+import { useMemo, type RefObject } from "react";
+import { Group, Transformer } from "react-konva";
+import type Konva from "konva";
+import type { KonvaEventObject } from "konva/lib/Node";
+import type { PesPatternData } from "../../formats/import/pesImporter";
+import {
+ calculatePatternCenter,
+ convertPenStitchesToPesFormat,
+} from "./patternCanvasHelpers";
+import { Stitches, PatternBounds, CurrentPosition } from "../KonvaComponents";
+
+interface PatternLayerProps {
+ pesData: PesPatternData;
+ offset: { x: number; y: number };
+ rotation?: number;
+ isInteractive: boolean;
+ showProgress?: boolean;
+ currentStitchIndex?: number;
+ patternGroupRef?: RefObject<Konva.Group | null>;
+ transformerRef?: RefObject<Konva.Transformer | null>;
+ onDragEnd?: (e: Konva.KonvaEventObject<DragEvent>) => void;
+ onTransformEnd?: (e: KonvaEventObject<Event>) => void;
+ attachTransformer?: () => void;
+}
+
+export function PatternLayer({
+ pesData,
+ offset,
+ rotation = 0,
+ isInteractive,
+ showProgress = false,
+ currentStitchIndex = 0,
+ patternGroupRef,
+ transformerRef,
+ onDragEnd,
+ onTransformEnd,
+ attachTransformer,
+}: PatternLayerProps) {
+ const center = useMemo(
+ () => calculatePatternCenter(pesData.bounds),
+ [pesData.bounds],
+ );
+
+ const stitches = useMemo(
+ () => convertPenStitchesToPesFormat(pesData.penStitches),
+ [pesData.penStitches],
+ );
+
+ const groupName = isInteractive ? "pattern-group" : "uploaded-pattern-group";
+
+ console.log(
+ `[PatternLayer] Rendering ${isInteractive ? "original" : "uploaded"} pattern:`,
+ {
+ position: offset,
+ rotation: isInteractive ? rotation : "n/a",
+ center,
+ bounds: pesData.bounds,
+ },
+ );
+
+ return (
+ <>
+ <Group
+ name={groupName}
+ ref={
+ isInteractive
+ ? (node) => {
+ if (patternGroupRef) {
+ patternGroupRef.current = node;
+ }
+ // Set initial rotation from state
+ if (node && isInteractive) {
+ node.rotation(rotation);
+ // Try to attach transformer when group is mounted
+ if (attachTransformer) {
+ attachTransformer();
+ }
+ }
+ }
+ : undefined
+ }
+ draggable={isInteractive}
+ x={offset.x}
+ y={offset.y}
+ offsetX={center.x}
+ offsetY={center.y}
+ onDragEnd={isInteractive ? onDragEnd : undefined}
+ onTransformEnd={isInteractive ? onTransformEnd : undefined}
+ onMouseEnter={
+ isInteractive
+ ? (e) => {
+ const stage = e.target.getStage();
+ if (stage) stage.container().style.cursor = "move";
+ }
+ : undefined
+ }
+ onMouseLeave={
+ isInteractive
+ ? (e) => {
+ const stage = e.target.getStage();
+ if (stage) stage.container().style.cursor = "grab";
+ }
+ : undefined
+ }
+ >
+ <Stitches
+ stitches={stitches}
+ pesData={pesData}
+ currentStitchIndex={currentStitchIndex}
+ showProgress={showProgress}
+ />
+ <PatternBounds bounds={pesData.bounds} />
+ </Group>
+
+ {/* Transformer only for interactive layer */}
+ {isInteractive && transformerRef && (
+ <Transformer
+ ref={(node) => {
+ if (transformerRef) {
+ transformerRef.current = node;
+ }
+ // Try to attach transformer when transformer is mounted
+ if (node && attachTransformer) {
+ attachTransformer();
+ }
+ }}
+ enabledAnchors={[]}
+ rotateEnabled={true}
+ borderEnabled={true}
+ borderStroke="#FF6B6B"
+ borderStrokeWidth={2}
+ rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]}
+ ignoreStroke={true}
+ rotateAnchorOffset={20}
+ />
+ )}
+
+ {/* Current position indicator (only for uploaded pattern with progress) */}
+ {!isInteractive && showProgress && currentStitchIndex > 0 && (
+ <Group x={offset.x} y={offset.y} offsetX={center.x} offsetY={center.y}>
+ <CurrentPosition
+ currentStitchIndex={currentStitchIndex}
+ stitches={stitches}
+ />
+ </Group>
+ )}
+ </>
+ );
+}
Served by rngit 1.3.3 - Generated in 0.04s